From f9bfc82880212e1a13f6bbb28ecfc87b89346f26 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 23 Jul 2025 06:06:27 +0000 Subject: (김준회) 메뉴접근제어(부서별) 메뉴 구현 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../department-domain-assignment-dialog.tsx | 384 +++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx') diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx new file mode 100644 index 00000000..277511cb --- /dev/null +++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-domain-assignment-dialog.tsx @@ -0,0 +1,384 @@ +"use client"; + +import * as React from "react"; +import { Loader2, Users, Building2, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DepartmentNode +} from "@/lib/users/knox-service"; +import { + getDepartmentDomainAssignmentsByDepartments, + type UserDomain +} from "@/lib/users/department-domain/service"; +import { DOMAIN_OPTIONS, getDomainLabel } from "./domain-constants"; + +interface ExistingAssignment { + id: number; + companyCode: string; + departmentCode: string; + departmentName: string; + assignedDomain: string; + description?: string | null; + createdAt: Date; + updatedAt: Date; +} + +interface DepartmentDomainAssignmentDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedDepartments: string[]; + departments: DepartmentNode[]; + companyInfo: { code: string; name: string }; + onAssign: (assignments: { + departmentCodes: string[]; + domain: string; + description?: string; + }) => Promise; + isLoading?: boolean; +} + +export function DepartmentDomainAssignmentDialog({ + open, + onOpenChange, + selectedDepartments, + departments, + companyInfo, + onAssign, + isLoading = false, +}: DepartmentDomainAssignmentDialogProps) { + const [selectedDomain, setSelectedDomain] = React.useState(""); + const [description, setDescription] = React.useState(""); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [existingAssignments, setExistingAssignments] = React.useState([]); + const [isLoadingAssignments, setIsLoadingAssignments] = React.useState(false); + + // 선택된 부서들의 정보 가져오기 + const getSelectedDepartmentInfo = React.useCallback(() => { + const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { + for (const node of nodes) { + if (node.departmentCode === code) { + return node; + } + const found = findDepartment(node.children, code); + if (found) return found; + } + return null; + }; + + return selectedDepartments + .map(code => findDepartment(departments, code)) + .filter(Boolean) as DepartmentNode[]; + }, [departments, selectedDepartments]); + + // 회사별로 그룹화 + const selectedDepartmentsByCompany = React.useMemo(() => { + const deptInfo = getSelectedDepartmentInfo(); + const grouped = new Map(); + + deptInfo.forEach(dept => { + if (!grouped.has(dept.companyCode)) { + grouped.set(dept.companyCode, []); + } + grouped.get(dept.companyCode)!.push(dept); + }); + + return grouped; + }, [getSelectedDepartmentInfo]); + + // 기존 할당 정보 조회 + React.useEffect(() => { + if (open && selectedDepartments.length > 0) { + const loadExistingAssignments = async () => { + setIsLoadingAssignments(true); + try { + const assignments = await getDepartmentDomainAssignmentsByDepartments(selectedDepartments); + setExistingAssignments(assignments as ExistingAssignment[]); + } catch (error) { + console.error("기존 할당 정보 조회 실패:", error); + setExistingAssignments([]); + } finally { + setIsLoadingAssignments(false); + } + }; + + loadExistingAssignments(); + } else { + setExistingAssignments([]); + } + }, [open, selectedDepartments]); + + // 폼 초기화 + React.useEffect(() => { + if (open) { + setSelectedDomain(""); + setDescription(""); + setIsSubmitting(false); + } + }, [open]); + + // 할당 처리 + const handleAssign = async () => { + if (!selectedDomain || selectedDepartments.length === 0) { + return; + } + + setIsSubmitting(true); + + try { + await onAssign({ + departmentCodes: selectedDepartments, + domain: selectedDomain, + description: description.trim() || undefined, + }); + + // 성공 시 다이얼로그 닫기 + onOpenChange(false); + } catch (error) { + console.error("도메인 할당 실패:", error); + } finally { + setIsSubmitting(false); + } + }; + + const canSubmit = selectedDomain && selectedDepartments.length > 0 && !isSubmitting && !isLoading; + const selectedDomainInfo = DOMAIN_OPTIONS.find(opt => opt.value === selectedDomain); + const hasConflicts = existingAssignments.some(a => a.assignedDomain !== selectedDomain && selectedDomain); + + return ( + + + + + + 부서별 도메인 할당 + + + 선택된 {selectedDepartments.length}개 부서에 도메인을 할당합니다. + 상위 부서를 선택한 경우 하위 부서들도 자동으로 포함됩니다. + + + +
+ +
+ {/* 선택된 부서들 표시 */} +
+ + +
+ {Array.from(selectedDepartmentsByCompany.entries()).map(([companyCode, depts]) => ( +
+
+ {companyCode} - {companyInfo.name} +
+
+ {depts.map((dept) => ( + + {dept.departmentName || dept.departmentCode} + + ))} +
+
+ ))} +
+
+ + {/* 기존 할당 현황 */} + {(existingAssignments.length > 0 || isLoadingAssignments) && ( + <> + +
+ + + {isLoadingAssignments ? ( +
+ + 기존 할당 정보를 조회하는 중... +
+ ) : ( +
+ + + + 부서 + 현재 도메인 + 할당일 + 설명 + + + + {existingAssignments.map((assignment) => ( + + + {assignment.departmentName} + + + + {getDomainLabel(assignment.assignedDomain)} + + + + {new Date(assignment.createdAt).toLocaleDateString('ko-KR')} + + + {assignment.description || '-'} + + + ))} + +
+
+ )} + + {hasConflicts && ( +
+
+ +
+
도메인 변경 주의
+
+ 일부 부서의 기존 도메인과 다른 도메인을 할당하려고 합니다. + 기존 할당은 자동으로 비활성화됩니다. +
+
+
+
+ )} +
+ + )} + + + + {/* 도메인 선택 */} +
+ + + + {selectedDomainInfo && ( +
+ + {selectedDomainInfo.label} + + {selectedDomainInfo.description} +
+ )} +
+ + {/* 할당 사유/설명 */} +
+ +